package top.codetheory.thread._002_保护性暂停;

import java.util.Hashtable;
import java.util.Map;
import java.util.Set;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.Setter;
import lombok.extern.slf4j.Slf4j;

/**
 * @author: CodeTheory
 *
 * <p>
 * <img src="https://gitee.com/codetheory/img-on1/raw/master/img01/1714115363009-2024-4-2615:09:23.png"  />
 * </p>
 * <p>
 * 解释:
 *      Mailboxes: 信箱, 里面的id对应的是people的门牌号
 *      Guard: 楼下门卫, 负责管理信箱, 还有一个告示栏, 里面写了每个信箱对应的门牌号
 *      People: 居民, 有自己的房间号, 他们会去楼下的信箱里找自己的信  如果没有信箱就需要去门卫注册一个
 *      Postman: 邮递员, 负责送信, 他会根据门卫的告示栏信息找到对应的邮箱, 然后把信件放进去
 * </p>
 * <p>
 * 模拟场景:
 *      people:听说有人给他寄信了, 所以想去楼下门卫那里安装一个信箱
 *      门卫:收到安装信箱的申请, 给他分配了一个信箱, 并且信箱上贴了people的门牌号, 然后将这个信箱的信息公示了出去
 *      postman:分配到了一份寄到某某门牌号的信, 需要去送, 直接从门卫那里的告示栏里找到了这个信箱, 然后塞了进去
 *      people:从邮箱里面拿出信
 * </p>
 **/
public class _002_多任务版本_信箱 {
    public static void main(String[] args) throws InterruptedException {
        for (int i = 0; i < 3; i++) {
            People people = new People();
            people.setName("people-" + i);
            people.start();
        }

        Thread.sleep(1000);

        for (Integer id : Guard.getIds()) {
            Postman postman = new Postman(id, "内容" + id);
            postman.setName("postman-" + id);
            postman.start();
        }
    }
}


/**
 * 信箱
 */
class Mailbox {
    private Object response; // 信的内容
    @Getter
    @Setter
    private int id; // 需要送到的门牌号哦啊

    public Object getMail(long millis) {
        synchronized (this) {  // wait 需要加锁
            long begin = System.currentTimeMillis();
            while (response == null) {  // 使用 while 防止虚假唤醒

                if (millis <= 0) {  // 如果剩余等待时间小于等于0, 直接返回
                    break;
                }

                try {
                    this.wait(millis);  // 需要计算剩余等待时间, 不然虚假唤醒的话, 会再等待 millis 时间
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
                millis -= System.currentTimeMillis() - begin;  // 计算剩余等待时间
            }
            // 不能直接返回, 需要等待结果输出之后才能返回, 所以上面要加wait
            return response;
        }
    }

    public void sendMail(Object response) {
        synchronized (this) {
            this.response = response;
            this.notifyAll();
        }
    }

    public Mailbox(int id) {
        this.id = id;
    }
}

/**
 * 门卫
 */
class Guard {
    // HashTable 是线程安全的
    // 告示栏 存储的内容是 门牌号 - 信箱地址
    private static final Map<Integer, Mailbox> boxes = new Hashtable<>();
    // 门牌号
    private static int id = 1;

    /**
     * 产生唯一id
     * 因为i++不是原子性的, 所以需要加锁
     *
     * 这里可以理解为门卫自动获取到你的门牌号
     */
    public static synchronized int generateId() {
        return id++;
    }

    /**
     * 获取门牌号对应的信箱地址
     */
    public static Mailbox getMailboxAddress(int id) {
        return boxes.remove(id);
    }

    /**
     * 门卫分配信箱
     */
    public static Mailbox createMailbox() {
        Mailbox go = new Mailbox(generateId());
        boxes.put(go.getId(), go);
        return go;
    }

    public static Set<Integer> getIds() {
        return boxes.keySet();
    }

}

@Slf4j
class People extends Thread {
    @Override
    public void run() {
        // (1) 去门卫那里注册一个信箱, 门卫会根据你的门牌号给你分配一个信箱
        Mailbox go = Guard.createMailbox();
        log.info("开始收信, 门牌号: {} 这里等", go.getId());
        // (2) 等待接受信件
        Object response = go.getMail(5000);
        log.info("收到信,门牌号: {},信的内容: {}", go.getId(), response);
    }
}

@Slf4j
@AllArgsConstructor
class Postman extends Thread {

    private int id;
    private String mail;

    @Override
    public void run() {
        // (3) 去门卫那里找到门牌号对应的信箱
        Mailbox go = Guard.getMailboxAddress(id);
        log.info("送信, 门牌号:{}, 信件内容: {}", go.getId(), mail);
        // (4) 把信塞进去
        go.sendMail(mail);
    }
}